#' LaTeX regression tables
#'
#' @param x a model object, \code{wickr::\link[wickr]{sumer}} object, or \code{list} thereof
#' @param .captn character. Caption for the table. Default \code{NULL}.
#' @param .paren character. Value to parenthesize. \code{"p.value"}, \code{"none"}, or \code{"std.error"} (the default).
#' @param .starz numeric.   Significance levels. Default \code{c(0.05, 0.01, 0.001)}.
#' @param .charz character. Symbols for sugbufucabce kevels. Default \code{c("*", "**", "***")}.
#' @param .digit numeric.   Number of places to round decimals to. Default \code{3}.
#' @param .above character. Names of model metadata values to include ABOVE table. Default \code{NULL}.
#' @param .below character. Names of model metadata values to include BELOW table. Default \code{NULL}.
#' @param .place character. LaTeX table placement. Default \code{"!htb"}.
#' @param .tomit character. (Partial) names of terms to omit from table. Default \code{NULL}.
#' @param .comit character. (Partial) names of _components_ to omit from table. Default \code{NULL}.
#' @param .scien logical.   Use scientific notation? Default \code{NA} (auto).
#' @param .ndrop logical.   Leave a note about dropped columns? Actually rather annoying. Default \code{FALSE}.
#' @param .incol character. Columns by which to widen data. Only \code{"outcome"} supported. Default \code{NULL}.
#' @param .merge character. merge \code{level} into \code{term}? Default \code{FALSE}.
#' @param .float logical.   Do we all float down here? Default \code{TRUE}.
#' @param .order character. Terms to pull to the top of the table, in order. Default \code{NULL}.
#' @param .naout logical.   Drop rows for vars with NA estimates? Don't actually do this. Default \code{FALSE}.
#' @param .print logical.   Barf the result to the screen? \code{FALSE} to return _only_. Default \code{TRUE}.
#' @param .oterm logical.   Omit the \code{term} column if only one term is reported? Default \code{FALSE}.
#' @param .ttype character. Table type. \code{"latex"} or \code{"html"}. Default \code{"latex"}.
#' @param .space character. Space between rows, in LaTeX notation. Default \code{"2.25ex"}.
#' @param .title character. Columns to transform to title case. Default \code{NULL}.
#' @param .capup logical.   Caption at top of table if \code{.float==FALSE}? Default \code{FALSE} (at bottom).
#' @param .headr logical.   Display hierarchical header? Default \code{TRUE}.
#' @param .check character. Hierarchical organization columns to display below instead of above table. Default \code{NULL}.
#' @param .hinot character. STuff to put atop the hierarchical header (if any). Default \code{NULL}.
#' @param .point logical.   Align columns on decimal point? Default \code{TRUE}.
#' @param .notit logical.   Notes at bottom in italics? Default \code{FALSE}.
#' @param .toppr character. LaTeX code to put at the very top of the table. Default \code{NULL}.
#' @param .modno logical.   Number models? Default \code{TRUE}.
#' @param .align character. Alignment of left-side columns. Default \code{"r"}.
#' @param .tbold character. Bold these terms. Default \code{setdiff(.order, "(Intercept)")}.
#' @param .snote logical.   Note what \code{.charz} go with what \code{.starz}? Default \code{TRUE}.
#' @param .pnote logical.   Note what quantity is in parentheses? Default \code{FALSE}.
#' @param .sumer function.  This is a bad design decision. Don't change this. Default \code{wickr::sumer}.
#' @param ... other arguments passed to \code{\link[xtable]{print.xtable}} and/or \code{\link[wickr]{sumer}}.
#' @return whatever \code{print.xtable} returns
#' @export
galaxy <- function(x,
                   .captn = NULL,
                   .paren = "std.error",
                   .starz = c(0.05, 0.01, 0.001),
                   .charz = c("*", "**", "***"),
                   .digit = 3,
                   .above = NULL,
                   .below = NULL,
                   .place = "!htb",
                   .tomit = NULL,
                   .comit = NULL,
                   .scien = NA,
                   .ndrop = FALSE,
                   .incol = character(0),
                   .merge = FALSE,
                   .float = TRUE,
                   .order = NULL,
                   .naout = FALSE,
                   .print = TRUE,
                   .oterm = FALSE,
                   .ttype = "latex",
                   .space = "2.25ex",
                   .title = NULL,
                   .capup = FALSE,
                   .headr = TRUE,
                   .check = NULL,
                   .hinot = NULL,
                   .point = TRUE,
                   .notit = FALSE,
                   .toppr = NULL,
                   .modno = TRUE,
                   .snote = TRUE,
                   .pnote = FALSE,
                   .align = "r",
                   .tbold = setdiff(.order, "(Intercept)"),
                   .sumer = wickr::sumer,
                   ...) {

  #### ensure valid values ----

  .paren <- if(.paren %in% c("p.value", "std.error", "none")) {.paren} else {
    warning(paste(.paren, "isn't an option for the parenthesized value; defaulting to 'std.error'"))
    "std.error"
  }

  .stord <- order(.starz, decreasing = TRUE)
  .charz <- c("",  .charz[.stord])
  .starz <- c(Inf, .starz[.stord])

  .merge <- any(isTRUE(as.logical(.merge))) | any(.merge == "level") # permissive but w/e

  .toppr <- Reduce(paste0, .toppr)

  #### Extraction block ----

  # x <- if(!(methods::is(x, "list") | methods::is(x, "tbl"))) {list(x)} else {x}

  y <- .sumer(x, ...) ## a tidy summary of each model. recurses over lists of models

  if("component" %in% colnames(y)) {y$component[is.na(y$component)] <- "mean"} # allow beta model tables side by side with others

  z <- attr(y, "sumer")     ## a table of metadata values for the models in x

  #### First munging block
  #### TODO: make the steps in this block less crap

  # drop rows for vars with NA coefs if told to (though what a poor idea)
  y <- if(.naout) {dplyr::filter(y, !is.na(.data$estimate))} else {y}

  ## suppress unwanted terms
  omits <- lapply(.tomit, stringr::str_detect, string=y$term)
  omits <- lapply(omits, which)
  omits <- Reduce(union, omits)

  y <- y[setdiff(seq_along(y$term), omits), ]

  ## suppress unwanted components
  omits <- lapply(lapply(.comit, purrr::quietly(stringr::str_detect), string=y$component), `[[`, i="result")
  omits <- lapply(omits, which)
  omits <- Reduce(union, omits)

  y <- if("component" %in% colnames(y)) {y[setdiff(seq_along(y$component), omits), ]} else {y}

  # NOW merge levels into term column if told to
  # AND        empty out level column if told to
  y <- if(.merge) {dplyr::mutate(y, term = ifelse(is.na(.data$level), .data$term, .data$level), level = NA)} else {y}

  #### Experiment ----

  # drop non-varying non-essential columns
  Numcols <- intersect(c("estimate", "std.error", "statistic", "p.value"), colnames(y))
  Yescols <- intersect(c("term"), colnames(y))
  Yescols <- c(Yescols, Numcols)
  Zedcols <- intersect(colnames(z), colnames(y))

  # unset (and preserve) columns that don't vary
  y <- dplyr::group_by(y)
  y <- dplyr::group_by(y, dplyr::across(tidyselect::all_of(Yescols)))
  v <- dplyr::select(y, tidyselect:::where(function(i) {length(table(i)) <= 1}))
  y <- dplyr::select(y, tidyselect:::where(function(i) {length(table(i)) >  1}))

  v <- dplyr::group_by(v)
  v <- dplyr::select(v, tidyselect::any_of(setdiff(colnames(v), Yescols)))
  v <- dplyr::group_by_all(v)
  v <- dplyr::summarise(v)
  v <- tidyr::gather(v, "fixcols", "fixvals")

  # numeric columns to the right
  y <- dplyr::group_by(y)
  y <- dplyr::group_by(y, dplyr::across(tidyselect::all_of(setdiff(colnames(y), Numcols))))
  y <- dplyr::select(y, tidyselect::any_of(Numcols))

  # occasional (?) columns to the right
  # TODO: carve outcome back out, conditionally
  Occcols <- intersect(c("outcome", "component", "term", "level", "baseline", "versus"), colnames(y))
  Occcols <- setdiff(Occcols, .incol)
  Allcols <- c(Occcols, Numcols)
  y <- dplyr::group_by(y)
  y <- dplyr::group_by(y, dplyr::across(tidyselect::all_of(setdiff(colnames(y), Allcols))))
  y <- dplyr::select(y, tidyselect::any_of(Allcols))

  Rowcols <- dplyr::group_vars(y)

  z <- dplyr::group_by(z)
  z <- dplyr::group_by(z, dplyr::across(intersect(tidyselect::all_of(Rowcols), colnames(z))))
  z <- dplyr::select(z, tidyselect::any_of(setdiff(colnames(z), setdiff(Zedcols, colnames(y)))))

  # z <- dplyr::group_by(z)
  # z <- dplyr::group_by(z, dplyr::across(tidyselect::all_of(Rowcols)))
  z <- dplyr::select(z, tidyselect::any_of(setdiff(colnames(z), Rowcols)))

  # get out model labels and assign numbers
  W <- dplyr::summarise(y)
  W <- dplyr::group_by(W)
  W <- dplyr::mutate(W, modno__ = dplyr::row_number())
  W <- dplyr::group_by(W, .data$modno__)
  W <- dplyr::select(W, tidyselect::any_of(setdiff(colnames(W), "modno__")))

  # replace model labels with model numbers in y and z
  y <- dplyr::left_join(W, y)
  z <- dplyr::left_join(W, z) # this will duplicate metadata where .incol == "outcome" but oh well

  y <- dplyr::select(y, tidyselect::any_of(setdiff(colnames(y), Rowcols)))
  z <- dplyr::select(z, tidyselect::any_of(setdiff(colnames(z), Rowcols)))

  # W <- if(.modno) {W} else {dplyr::select_at(W, setdiff(colnames(W), dplyr::last(Rowcols)))}

  WW <- W

  # order threshold columns to bottom
  # TODO: this isn't done right
  # and it's super not clear why it's done here instead of only later
  y <- dplyr::mutate(y, is_threshold__ = stringr::str_detect(.data$term, stringr::fixed("|")))
  y <- dplyr::arrange(y, dplyr::across(tidyselect::any_of(intersect(c("modno__", Occcols, "is_threshold__"), colnames(y)))))
  y <- dplyr::select(y, -.data$is_threshold__)

  # turn NA in character columns into "..." and turn all NAs into NA
  y <- dplyr::mutate(y, dplyr::across(tidyselect:::where(is.factor), as.character))
  y <- dplyr::mutate(y, dplyr::across(tidyselect::all_of(setdiff(colnames(y), c("modno__", Numcols))),
                                      function(i) {ifelse(is.na(i), "...", i)}))
  y[is.na(y)] <- NA

  ## if the column to be included in parentheses doesn't exist, forget about it
  .paren <- intersect(.paren, colnames(y))

  ## format the numeric values that will be retained in the table
  y <- dplyr::mutate(y, " " = .charz[rowSums(outer(.data$p.value, .starz, "<"))])
  y <- dplyr::mutate(y, " " = ifelse(is.na(.data$` `), "", .data$` `))
  y <- dplyr::mutate(y, estimate = paste0(.round(.data$estimate, .digit, .scien=.scien), "^{", .data$` `, "}"))
  y <- dplyr::mutate(y, dplyr::across(tidyselect::all_of(.paren), function(i) {paste0("(", .round(i, .digit, .scien=.scien), ")")}))

  y <- dplyr::select(y,
                     -tidyselect::any_of(setdiff(Numcols, c("estimate", .paren))),
                     -.data$` `)

  #### End first munging block ----

  ## THE MOMENT OF TRUTH!
  ## TURN IT ON ITS SIDE! ----
  ## TODO: pivot to tidyr::pivot_wider() and tidyr::pivot_longer()
  Y <- tidyr::pivot_longer(y, tidyselect::one_of(intersect(c(Numcols, " "), colnames(y))), "variable")
  Y <- dplyr::mutate(Y, value = stringr::str_replace(.data$value, "^\\(NA\\)$", ""))
  Y <- dplyr::group_by(Y)
  Y <- dplyr::select(Y, -tidyselect::any_of(Rowcols))
  Y <- dplyr::mutate(Y, dplyr::across(tidyselect::all_of(setdiff(colnames(Y), c("modno__", "variable", "value"))), .fsort))
  Y <- dplyr::mutate(Y, value = if(.point) {.data$value} else {paste0("\\multicolumn{1}{c}{$", .data$value, "$}")})
  Y <- tidyr::pivot_wider(Y, names_from = .data$modno__, values_from = .data$value)

  ## Turn the metadata its side, too
  z <- dplyr::mutate(z, dplyr::across(tidyselect::everything(), as.character))
  z <- tidyr::nest(z) # I have ceased to understand what this nesting and unnesting is for
  z <- dplyr::mutate(z, data = list(tidyr::pivot_longer(dplyr::first(.data$data), tidyselect::everything(), "term", values_to="val")))
  z <- tidyr::unnest(z, .data$data)
  z <- dplyr::filter_all(z, function(i) {!is.na(i)})
  z <- suppressWarnings(dplyr::mutate(z, val = paste0("\\multicolumn{1}{c}{", .round(.data$val, .digit, 0, .scien=.scien), "}")))
  z <- dplyr::group_by(z)
  z <- dplyr::mutate(z, term = .fsort(.data$term))
  Z <- tidyr::pivot_wider(z, names_from = .data$modno__, values_from = .data$val)

  ## Process the labels
  U <- dplyr::select(W, tidyselect::any_of(.check))
  U <- dplyr::mutate(U, OK____ = TRUE) # don't remember what this does
  U <- dplyr::mutate(U, dplyr::across(tidyselect:::where(is.logical), function(i) {ifelse(i, "\\checkmark{}", "")}))
  U <- tidyr::pivot_longer(U, -.data$modno__, "term", values_to = "val")
  U <- dplyr::mutate(U, val = paste0("\\multicolumn{1}{c}{", .data$val, "}"))
  U <- tidyr::pivot_wider(U, names_from = .data$modno__, values_from = .data$val)
  U <- dplyr::filter(U, !.data$term == "OK____")

  W <- dplyr::group_by(W)
  W <- dplyr::mutate(W, OK____ = TRUE)
  W <- dplyr::mutate(W, OK____ = as.character(.data$OK____))
  W <- dplyr::select(W, -.data$modno__)
  W <- tidyr::pivot_longer(W, tidyselect::everything(), "col")
  W <- dplyr::group_by(W, .data$col)
  W <- dplyr::summarise(W, rle = list(rle(.data$value)), .groups = "keep")
  # W <- dplyr::group_by(W, .data$col)
  W <- dplyr::mutate(W,
                     rle = list(lapply(`names<-`(names(dplyr::first(.data$rle)), names(dplyr::first(.data$rle))),
                                       getElement,
                                       object=dplyr::first(.data$rle))))
  W <- dplyr::mutate(W, rle = list(dplyr::as_tibble(dplyr::first(.data$rle))))
  W <- tidyr::unnest(W, .data$rle)
  W <- dplyr::mutate(W, to = cumsum(.data$lengths) + length(Occcols))
  W <- dplyr::mutate(W, from = .data$to - c(dplyr::first(.data$to) - length(Occcols) - 1, diff(.data$to) - 1))
  W <- dplyr::mutate(W,
                     cell = paste0("\\multicolumn{", .data$lengths, "}{c}{", .data$values, "}"),
                     line = paste0("\\cmidrule(l){", .data$from, "-", .data$to, "}"))
  W <- dplyr::mutate(W, line = ifelse(.data$from < .data$to, .data$line, ""))

  H <- dplyr::summarise(W,
                        row = paste0(paste0(rep("\\multicolumn{1}{c}{ } & ", length(Occcols)), collapse = ""),
                                     paste0(.data$cell, collapse = " & "),
                                     " \\\\ ",
                                     paste0(.data$line, collapse = " ")))
  H <- dplyr::filter(H, !.data$col  ==  "OK____")
  H <- dplyr::mutate(H, col = factor(.data$col, levels=Rowcols))
  H <- dplyr::filter(H, !.data$col %in% .check)


  #### Second munging block ----

  # reorder terms (is this not done already?)
  Y <- dplyr::arrange(Y, !.data$term=="(Intercept)")                                  # intercept to top
  Y <- dplyr::mutate(Y, term = .fsort(as.character(.data$term), .order))              # choose this order
  Y <- dplyr::arrange(Y, .data$term)                                                  # order by this order (???)
  Y <- dplyr::arrange(Y, stringr::str_detect(.data$term, stringr::fixed("|")))        # threshold terms to bottom
  Y <- if("component" %in% colnames(Y)) {dplyr::arrange(Y, .data$component)} else {Y} # sort by component

  nterm <- length(table(Y$term))

  ## erase repeated values
  key_col <- which(colnames(Y)=="variable") - 1
  y_cols  <- ncol(Y) - key_col - 1
  # x_cols  <- (1:key_col)[-key_col] # wtf?
  x_cols  <- (1:key_col)

  # To Title Case
  Y[, intersect(colnames(Y)[x_cols], .title)] <- lapply(Y[, intersect(colnames(Y)[x_cols], .title), drop = FALSE],
                                                        stringr::str_to_title)

  Y <- data.frame(Y, check.names=FALSE)
  # Y <- dplyr::mutate(Y, term = ifelse(.data$term %in% .tbold, paste0("\\textbf{", .data$term, "}"), .data$term))
  Y <- dplyr::mutate(Y, sp____ = ifelse(.data$term == dplyr::first(.data$term), "2.25ex", .space))
  Y[                        , key_col] <- paste0("\\rule{0pt}{", Y$sp____, "}", Y[, key_col])
  Y <- dplyr::select(Y, -.data$sp____)
  Y[Y$variable != "estimate", key_col] <- ""

  Y <- dplyr::select(Y, -.data$variable)

  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], as.character)
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], function(x) { ## duplicated() ought to work here, but fails :(
    x[which(c(1, diff(as.numeric(as.factor(x)))) == 0)] <- ""
    x
  })


  ## do our own sanitizing
  ## TODO: must be much more comprehensive
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], stringr::str_replace_all, pattern="_", replacement=" ")
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], stringr::str_replace_all, pattern="%", replacement="\\\\%")
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], stringr::str_replace_all, pattern="#", replacement="\\\\#")
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], stringr::str_replace_all, pattern="&", replacement="\\\\&")
  Y[, x_cols] <- lapply(Y[, x_cols, drop=FALSE], stringr::str_replace_all, pattern="`", replacement="")

  # TODO: the rest of the above for the colnames as well
  # Y <- `colnames<-`(Y, stringr::str_replace_all(colnames(Y), "&", "\\\\&"))

  ## TODO: also sanitize the metadata

  ## retain only those metadata requested in the arguments
  Z <- Z[Z$term %in% .below, ]

  ## also mung the metadata
  Z$term <- as.character(Z$term)
  Z$term <- ifelse(Z$term==Z$term[1], paste0("\\rule{0pt}{2.25ex}", Z$term[1]), Z$term)
  Z$term <- as.character(Z$term) # sic

  U$term <- as.character(U$term)
  U$term <- ifelse(U$term==U$term[1], paste0("\\rule{0pt}{2.25ex}", U$term[1]), U$term)
  U$term <- as.character(U$term) # sic

  #### End second munging block

  rows <- nrow(Y)

  ## Bolt the metadata onto the main table
  ## TODO: should work with dplyr::bind_rows
  Y <- dplyr::bind_rows(Y, U, Z)

  ## Add notes

  z_rows <- nrow(Y)

  star_note <- paste(.charz, rep("p <", length(.starz)), .starz)[-1]
  star_note <- Reduce(.sslap, star_note)

  par_note  <- c("p.value" = "p-values", "std.error" = "standard errors")[.paren]
  par_note  <- paste(par_note, rep("in parentheses", length(par_note)))

  foo_note  <- Reduce(.sslap, c(if(.snote) {star_note}, if(.pnote) {par_note}))
  baz_note  <- if(.ndrop) {Reduce(.sslap, paste(v$fixcols, as.character(v$fixvals), sep=": "))} else {NULL}

  note      <- Reduce(.sslap, c(baz_note,
                                foo_note))

  note      <- paste0(rep(paste0("\\hline \\multicolumn{", key_col, "}{l}{",
                                 if(.notit) {"\\textit{"} else {""},
                                 "Note:",
                                 if(.notit) {"}"}         else {""},
                                 "}"),
                          length(note)),
                      rep(" & ", length(note)),
                      rep(paste0("\\multicolumn{", y_cols, "}{r}{"), length(note)),
                      note,
                      rep("} \\\\", length(note)))
  note      <- c(note, "")[1] # in case note comes out NULL

  ## Add nested column headers

  if(.modno) {} else {
    befor <- paste0("^", WW$modno__, "$")
    after <- dplyr::last(WW)
    Y <- `colnames<-`(Y, qdap::multigsub(befor, after, colnames(Y), fixed=FALSE))
    H <- dplyr::filter(H, !.data$row==dplyr::first(.data$row))
  }

  chead <- paste(rev(H$row), collapse = " ") # wait why rev

  ## Format upper note
  .hinot <- paste0(rep(paste0("\\multicolumn{", key_col, "}{c}{ } & \\multicolumn{", y_cols, "}{c}{"), length(.hinot)),
                   .hinot,
                   rep("}", length(.hinot)))
  .hinot <- paste0(.hinot, collapse = " \\\\ ")[sign(length(.hinot))]
  .hinot <- paste0(.hinot,
                   " \\\\ "[sign(length(.hinot))],
                   paste0("\\cmidrule(l){", key_col + 1, "-", ncol(Y), "}")[sign(length(.hinot))])

  #### MOVE THAT BUS!

  a <- xtable::xtable(Y[, 1:(key_col + y_cols)], .captn, align=c(rep(.align, key_col + 1), rep("d", y_cols))) # does siunitx not work?
  colnames(a) <- colnames(Y) # sic

  if(.oterm & (nterm < 2)) {a$term <- ""}

  colnames(a) <- stringr::str_replace(colnames(a), "^term$",      " ")
  colnames(a) <- stringr::str_replace(colnames(a), "^component$", "  ")

  pre <- if(.float) {} else {paste0("\\begin{center}\n", if(.capup) {paste0("\\captionof{table}{", .captn, "}\n")})}
  end <- if(.float) {} else {paste0(if(!.capup) {paste0("\\captionof{table}{", .captn, "}\n")}, "\\end{center}\n")}
  mid <- invisible(print(a, table.placement=.place, include.rownames = FALSE, comment=FALSE, ...,
                         type = .ttype,
                         file="/dev/null", # up yours, Windows
                         floating = .float,
                         hline.after = c(0, rows),
                         add.to.row = list(pos=list(-1,
                                                    -1,
                                                    -1,
                                                    min(rows + 1, z_rows[length(note)]),
                                                    z_rows[length(note)]),
                                           command=c(if(length(.toppr)) {.toppr}    else {""},
                                                     if(length(.hinot)) {.hinot}    else {""},
                                                     if(.headr)         {chead}     else {""},
                                                     if(sign(nrow(U)))  {"\\hline"} else {""},
                                                     as.character(note))),
                         sanitize.text.function = function(x) {x},
                         sanitize.colnames.function = function(x) {paste0("\\multicolumn{1}{c}{", x, "}")}))

  if(.print) {
    cat(pre)
    cat(mid)
    cat(end)
  }

  invisible(paste(pre, mid, end)) # now returns something
}

## TODO: hang on, nsmall < .digit helps in integer cases but hurts in others
## maybe figure out how to do this on a per-column basis for the unrotated metadata
.round <- function(x, .digit, nsmall=.digit, .scien=.scien) {
  y <- as.numeric(x)
  ifelse(is.na(y), x, sapply(round(y, .digit), format, nsmall=nsmall, scientific=.scien))
}

.slap  <- function(x, y) {paste(x, y, sep="___")}
.sslap <- function(x, y) {paste(x, y, sep="; ")}
.cslap <- function(x, y) {paste(x, y, sep=": ")}

# recursive function to create hierarchical column headers
.chead <- function(foo) {
  bar <- sapply(foo, `[`, i=1)

  if(all(is.na(bar))) {return(NULL)}

  bar[is.na(bar)] <- " "

  # baz <- table(bar, useNA="ifany")[unique(bar)]
  baz <- rle(bar)

  # mouthful
  quack <- paste(
    paste0(
      "\\multicolumn{",
      baz$lengths,
      "}{c}{",
      baz$values,
      "}",
      collapse = " & "),
    " \\\\ ",
    paste0(
      paste0(
        "\\cmidrule(l){",
        cumsum(baz$lengths) - baz$lengths + 1,
        "-",
        cumsum(baz$lengths)
      )[baz$values != " "],
      "}",
      collapse=" "
    )
  )

  c(.chead(lapply(foo, `[`, i=-1)), quack)
}

# factor but take the levels in the order they're presented instead of sorting
.fsort <- function(x, i = character(0)) {factor(x, levels=c(intersect(i, unique(x)), setdiff(unique(x), i)))}

.lifna <- function(l, r) {ifelse(is.na(r), as.character(l), as.character(r))}

#' #' @importFrom rlang .data
#' #' @export
#' rlang::.data
#'

